In this post, we’ll walk through the design and implementation of a flexible, reusable Matching Pairs component for .NET MAUI. This journey was driven by real-world requirements. Iterative improvements contributed as well. The result is a highly customizable and MVVM-friendly UI control. It is suitable for educational games, language learning, and more.

The full source code is available on GitHub.
The Goal
We wanted a component that:
- Displays two columns of selectable items (e.g., words or phrases).
- Lets the user select one item from each column to attempt a match.
- Provides immediate visual feedback (color changes) for correct and incorrect matches.
- Disables matched pairs and tracks the number of attempts.
- Exposes a customizable “Continue” button, only enabled when all pairs are matched.
- Is highly customizable via bindable properties for colors, styles, text, and behavior.
- Supports both MVVM commands and event handlers for button actions.
Step 1: The Data Model
We started with a simple MatchingPair
class, then extended it to support UI state and property change notifications:
public class MatchingPair : INotifyPropertyChanged
{
public string Left { get; set; }
public string Right { get; set; }
// ... properties for IsMatched, IsLeftEnabled, IsRightEnabled, LeftColor, RightColor, etc. // Implements INotifyPropertyChanged for UI updates }
}
Each item is rendered with a Border
and a Label
, with colors and enabled state bound to the model.
Step 2: The UI Layout
The component’s XAML uses a Grid
with two CollectionView
s for the left and right columns, and a Button
for confirmation:
<components:MatchingPairsView
ButtonConfirmCommand="{Binding OnContinueCommand}"
ButtonConfirmText="Next"
CorrectColor="LimeGreen"
DefaultBackgroundColor="White"
DefaultStrokeColor="Gray"
DefaultTextColor="Black"
Delay="300"
Pairs="{Binding MyPairs}"
SelectedBorderColor="DodgerBlue"
ShowAttempts="True"
WrongColor="Crimson" />
Step 3: Customization via Bindable Properties
To make the component flexible, we exposed a wide range of bindable properties, including:
- Colors: For correct, wrong, selected, and default states (both border and text).
- Button: Text, style, command, and event handler.
- Delay: Feedback display duration (in ms).
- ShowAttempts: Whether to display the attempt count.
- AttemptCountStringFormat: Custom format for the attempt label.
- Pairs: The collection of pairs to match.
This allows consumers to fully tailor the component’s appearance and behavior.
Step 4: Selection and Matching Logic
The core logic ensures:
- Only one item per column can be selected at a time.
- When two items are selected,
TryMatch
is called. - The component disables both
CollectionView
s during feedback (using a private_isBusy
flag and a helper method). - Visual feedback is shown for a configurable delay.
- Matched pairs are disabled and colored appropriately.
- The attempt count is incremented and can be displayed.
- The “Continue” button is only enabled when all pairs are matched.
Example of the optimized TryMatch
method:
private async void TryMatch()
{
if (selectedLeft == null || selectedRight == null || _isBusy) return; _isBusy = true; SetCollectionsEnabled(false);
AttemptCount++;
var leftMatch = LeftWords.FirstOrDefault(x => x.Left == selectedLeft.Left);
var rightMatch = RightWords.FirstOrDefault(x => x.Right == selectedRight.Right);
if (leftMatch == null || rightMatch == null)
{
_isBusy = false;
SetCollectionsEnabled(true);
return;
}
if (selectedLeft.Right == selectedRight.Right)
{
// Correct match feedback
leftMatch.LeftColor = CorrectColor;
leftMatch.LeftTextColor = CorrectTextColor;
leftMatch.IsMatched = true;
leftMatch.IsLeftEnabled = false;
rightMatch.RightColor = CorrectColor;
rightMatch.RightTextColor = CorrectTextColor;
rightMatch.IsMatched = true;
rightMatch.IsRightEnabled = false;
}
else
{
// Wrong match feedback
leftMatch.LeftColor = WrongColor;
leftMatch.LeftTextColor = WrongTextColor;
rightMatch.RightColor = WrongColor;
rightMatch.RightTextColor = WrongTextColor;
}
await Task.Delay(Delay);
if (selectedLeft.Right != selectedRight.Right)
{
// Reset colors after wrong match
leftMatch.LeftColor = DefaultStrokeColor;
leftMatch.LeftTextColor = DefaultTextColor;
rightMatch.RightColor = DefaultStrokeColor;
rightMatch.RightTextColor = DefaultTextColor;
}
selectedLeft = null;
selectedRight = null;
LeftCollection.SelectedItem = null;
RightCollection.SelectedItem = null;
UpdateAllMatched();
if (!AllMatched)
SetCollectionsEnabled(true);
_isBusy = false;
}
Step 5: Button Actions
The “Continue” button supports both MVVM and code-behind patterns:
- ButtonConfirmCommand: Bind to an
ICommand
in your ViewModel. - ButtonConfirmClicked: Attach an event handler in code-behind.
Both are triggered when the button is clicked.
Step 6: Accessibility and Usability
- The component disables selection while feedback is being shown.
- Once all pairs are matched, the button is enabled and the collections are disabled.
- All UI text and colors are customizable for accessibility and localization.
Step 7: Usage in a Page
Here’s how you can use the component in your MAUI page:
<components:MatchingPairsView
ButtonConfirmCommand="{Binding OnContinueCommand}"
ButtonConfirmText="Next"
CorrectColor="LimeGreen"
DefaultBackgroundColor="White"
DefaultStrokeColor="Gray"
DefaultTextColor="Black"
Delay="300"
Pairs="{Binding MyPairs}"
SelectedBorderColor="DodgerBlue"
ShowAttempts="True"
WrongColor="Crimson" />
Step 8: Full List of Parameters
Property Name | Type | Default | Description |
---|---|---|---|
Pairs | ObservableCollection | — | The list of pairs to match. |
ShowAttempts | bool | true | Whether to display the attempt count label. |
AttemptCount | int | 0 | The number of attempts made (read-only, updated by the component). |
AllMatched | bool | false | Indicates if all pairs have been matched (read-only, updated by the component). |
Delay | int | 200 | Delay in milliseconds for feedback display after a match attempt. |
ButtonConfirmText | string | "Continue" | The text displayed on the confirm button. |
ButtonConfirmStyle | Style | null | The style applied to the confirm button. |
ButtonConfirmCommand | ICommand | null | Command executed when the confirm button is clicked. |
ButtonConfirmClicked | EventHandler | null | Event raised when the confirm button is clicked. |
CorrectColor | Color | Green | Border color for a correct match. |
CorrectTextColor | Color | Green | Text color for a correct match. |
WrongColor | Color | Red | Border color for an incorrect match. |
WrongTextColor | Color | Red | Text color for an incorrect match. |
SelectedBorderColor | Color | DodgerBlue | Border color for a selected item. |
SelectedTextColor | Color | DodgerBlue | Text color for a selected item. |
DefaultStrokeColor | Color | #FF444444 | Default border color for items. |
DefaultBackgroundColor | Color | White | Default background color for items. |
DefaultTextColor | Color | White | Default text color for items. |
Conclusion
Through iterative improvements and a focus on flexibility, we’ve built a robust Matching Pairs component for .NET MAUI. It’s easy to use, highly customizable, and ready for integration into your next educational or gamified app.
Happy coding!